package de.stefanocke.roo.wicket.base;

import static org.eclipse.xtext.xbase.lib.IterableExtensions.toList;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.eclipse.xtext.xbase.lib.Functions.Function1;
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails;
import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem;
import org.springframework.roo.metadata.MetadataItem;
import org.springframework.roo.model.JavaType;

import de.stefanocke.roo.wicket.MidUtils;
import de.stefanocke.roo.wicket.model.JavaTypeMetadataDetails;
import de.stefanocke.roo.wicket.model.ModelClassService;
import de.stefanocke.roo.wicket.registry.MetadataRegistry;

@Component(componentAbstract = true)
public abstract class ModelClassAwareItdMetadataProviderBase<M extends ItdMetadata, AV> extends
		ItdMetadataProviderBase<M, AV> {
	@Reference
	protected ModelClassService modelClassService;

	/**
	 * Use this constructor if you have multiple triggers. Override
	 * {@link #getTriggers()} then.
	 * 
	 * @param metadataClass
	 * @param annotationValuesClass
	 */
	public ModelClassAwareItdMetadataProviderBase(Class<M> metadataClass, Class<AV> annotationValuesClass) {
		this(null, metadataClass, annotationValuesClass, null);
	}

	public ModelClassAwareItdMetadataProviderBase(Class<?> triggerClass, Class<M> metadataClass,
			Class<AV> annotationValuesClass) {
		this(triggerClass, metadataClass, annotationValuesClass, null);
	}

	/**
	 * 
	 * @param triggerClass
	 *            the Annotation class that triggers this provider to create
	 *            metadata.
	 * @param metadataClass
	 *            the class of the metadata to create
	 * @param annotationValuesClass
	 *            the class to hold the attribute values of the trigger
	 *            annotation.
	 * @param kind
	 *            optionally the interface under wich the metadata shall be
	 *            registered in the {@link MetadataRegistry}. This allows
	 *            other metadata to search for some "kind" of metadata instead
	 *            of looking for a concrete class.
	 */
	public ModelClassAwareItdMetadataProviderBase(Class<?> triggerClass, Class<M> metadataClass,
			Class<AV> annotationValuesClass, Class<?> kind) {
		super(triggerClass, metadataClass, annotationValuesClass, kind);
	}

	protected ItdTypeDetailsProvidingMetadataItem getMetadataInternal(ItdMetadataCreationParams<AV> cmcp) {

		JavaTypeMetadataDetails modelClassTypeDetails = getModelClassTypeDetails(cmcp);
		return getMetadataInternal(new ModelClassAwareMetadataCreationParams<AV>(cmcp, modelClassTypeDetails));
	}

	protected abstract ItdTypeDetailsProvidingMetadataItem getMetadataInternal(
			ModelClassAwareMetadataCreationParams<AV> mcp);

	private JavaTypeMetadataDetails getModelClassTypeDetails(ItdMetadataCreationParams<AV> cmcp) {
		JavaType modelClass = getModelClass(cmcp);
		if (modelClass == null) {
			return null;
		}
		String mid = cmcp.getMid();
		// metadataRegistry.registerMetadata(mid, modelClass);
		JavaTypeMetadataDetails typeDetails = modelClassService.getJavaTypeMetadataDetails(mid, modelClass);
		if (typeDetails == null) {
			validationError(mid, "modelClass " + modelClass + " does not exist or is no application type.");
		}
		return typeDetails;
	}

	/**
	 * Determines the domain model class this metadata relates to.
	 * Usually, it is specified as an annotation value.
	 * Might be null, if this metadata is not related to a specific model class.
	 * 
	 * @param annotationValues
	 * @return
	 */
	protected JavaType getModelClass(ItdMetadataCreationParams<AV> cmcp) {
		AV annotationValues = cmcp.getAnnotationValues();
		if (!(annotationValues instanceof ModelClassAware)) {
			return null;
		}
		JavaType modelClass = ((ModelClassAware) annotationValues).getModelClass();
		if (modelClass == null) {
			validationError(cmcp.getMid(), "modelClass is required.");
		}
		return modelClass;
	}

	protected <T extends MetadataItem> T findWicketMetadata(String mid, JavaType modelClass,
			Class<T> metadataClass) {
		return findWicketMetadata(mid, modelClass, metadataClass, null, true);
	}
	
	protected <T extends MetadataItem> T findWicketMetadata(String mid, JavaType modelClass,
			Class<T> metadataClass, JavaType governor, boolean mandatory) {
		
		Function1<? super Iterable<T>,? extends Iterable<T>> filter = MetadataFilter.filterClosest(mid);
		return findWicketMetadata(mid, modelClass, metadataClass, governor, filter , true);
	}

	/**
	 * 
	 * @param mid
	 *            the client MID (for registering the dependency to the found
	 *            metadata)
	 * @param modelClass
	 *            the modelClass to find metadata for
	 * @param metadataClass
	 *            the kind of metadata to look for
	 * @param governor
	 *            to restrict the search to a certain governor. Might be null.
	 * @param filter 
	 * @param mandatory
	 *            if true, a validation exception will be thrown, if no metadata
	 *            is found.
	 * @return the found metadata
	 */
	@SuppressWarnings("unchecked")
	protected <T extends MetadataItem> T findWicketMetadata(String mid, JavaType modelClass,
			Class<T> metadataClass, JavaType governor,
			Function1<? super Iterable<T>,? extends Iterable<T>> filter, boolean mandatory) {
		T foundMd = null;

		Collection<T> candidatesWithRightType = new HashSet<T>();
		_if: if (governor == null) {
			// no governor was specified. Search for the best match.
			Set<String> candidates = metadataRegistry.findMetadata(mid, metadataClass, modelClass);
			//Set<T> candidateMetadata = new HashSet<T>();
			for (String candMid : candidates) {
				MetadataItem md = metadataService.get(candMid);
				if (md == null) {
					continue;
				}
				// if it's the same governor, always prefer it
				if (MidUtils.getJavaTypeForMid(mid).equals(MidUtils.getJavaTypeForMid(md.getId()))) {
					
					candidatesWithRightType = Collections.singleton((T) md);
					break _if;
				}
				if (md instanceof AutoWireSettings) {
					if (!((AutoWireSettings) md).isAutowireCandidate()) {
						continue;
					}
				}
				if (metadataClass.isInstance(md)) {
					candidatesWithRightType.add((T) md);
				}
			}
			if(filter!=null){
				candidatesWithRightType = toList(filter.apply(candidatesWithRightType));
			}
		} else {
			// a governor was specified. Search at that governor for the
			// requested metadata.
			ClassOrInterfaceTypeDetails govDetails = typeLocationService.getTypeDetails(governor);
			String candidateMid = MidUtils.get(metadataClass).createIdentifier(govDetails);
			MetadataItem md = metadataService.get(candidateMid);
			if (md != null && metadataClass.isInstance(md)) {
				if (modelClass != null
						&& (!(md instanceof ModelClassAware) || !modelClass.equals(((ModelClassAware) md)
								.getModelClass()))) {
					validationError(mid, md.getId() + " has no or wrong modelClass. Should be " + modelClass);
				} else {
					candidatesWithRightType = Collections.singleton((T) md);
				}
			}

		}

		if (candidatesWithRightType.size() > 1) {
//			for (T t : candidatesWithRightType) {
//				//register dependecies to get informed if the user fixes the ambiguity
//				registerDependency(t.getId(), mid);
//			}
			StringBuffer mids = new StringBuffer();
			for (T t : candidatesWithRightType) {
				mids.append(t.getId());
				mids.append(" (");
				mids.append(commonPackagePrefixLength(mid, t.getId()));
				mids.append(") ");
				if (t instanceof AutoWireSettings) {
					mids.append("autowire: " + ((AutoWireSettings) t).isAutowireCandidate() + " ");
				}
				

			}
			validationError(mid, "Multiple Metadata of type " + metadataClass.getSimpleName()
					+ " found for modelClass " + modelClass + ": " + mids);
		}
		if (candidatesWithRightType.isEmpty()) {
			if (mandatory || governor != null) {
				validationError(mid, "Metadata of type " + metadataClass.getSimpleName() + " not found for modelClass "
						+ modelClass
						+ (governor == null ? "." : ". Search has been restricted to governor " + governor));
			}
			return null;
		}
		foundMd = candidatesWithRightType.iterator().next();
		registerDependency(foundMd.getId(), mid);
		return foundMd;
	}

	/**
	 * The common package prefix length of the java types of to metadata items.
	 * Gets max, if it is the same java type.
	 * 
	 * @param mid
	 * @param candidateId
	 * @return
	 */
	private static int commonPackagePrefixLength(String mid, String candidateId) {
		String[] requestingType = MidUtils.getJavaTypeForMid(mid).getFullyQualifiedTypeName().split("[\\.]");
		String[] candidateType = MidUtils.getJavaTypeForMid(candidateId).getFullyQualifiedTypeName().split("[\\.]");
		int i = 0;
		for (String part : requestingType) {
			if (i >= candidateType.length || !part.equals(candidateType[i])) {
				break;
			}
			i++;
		}

		return i;
	}


}